package robocup.component.situations;

import java.util.EnumMap;

import robocup.component.SConf;
import robocup.component.WorldModel;
import robocup.component.ControllerNumbers.PLAY_MODE;
import robocup.component.geometry.Vektor;
import robocup.component.math.RCMath;
import robocup.component.worldobjects.Player;
import robocup.component.worldobjects.Player.UPDATE_MODE;

/**
 * This is our central evaluation function.
 * 
 * All Situations evaluate a number of possible actions each resulting in a new
 * situation. All of the resulting situations are then evaluated and the benefit
 * of each resulting situation is represented as a number between 0..1. As the
 * fine tuning of the different Situation Modesl is one of the most crucial and
 * complex problems in robocup we use a central approach for all models. Its
 * mainly used to decide what to do if the player has the ball.<br>
 * By now most of the values used in this class are set using 'human learning'.
 * However, we plan to find a better setup using machine learning approaches.
 * <br>
 * <br>
 * <b>TODO:</b> <br>
 * <ul>
 * <li>learn the weights used here (offline|online)</li>
 * </ul>
 */
public final class SituationEvaluator {

  private EnumMap<TYPE, Double> weights = new EnumMap<TYPE, Double>(TYPE.class);
  
  private final Vektor dummyVektor  = new Vektor();

  private final Vektor dummyVektor2 = new Vektor();

  private WorldModel wm;

  /**
   * the way the new situation is reached (high level action).
   */
  public enum TYPE {
    BALL_INTERCEPTION,
    DRIBBLING,
    WAIT_WITH_BALL,
    PASSING,
    KICKNRUN
  }

  /**
   * Contructor.
   * 
   * @param world - the worldmodel
   */
  public SituationEvaluator(final WorldModel world){
    this.wm = world;
    
    weights.put(TYPE.BALL_INTERCEPTION, 1.0);
    weights.put(TYPE.DRIBBLING, 1.0);
    weights.put(TYPE.WAIT_WITH_BALL, 1.0);
    weights.put(TYPE.PASSING, 1.0);
    weights.put(TYPE.KICKNRUN, 1.0);
    
  }
  
  /**
   * returns the benefit of a situation based on some meta data. by now it is
   * only used for situations with ball control.
   * 
   * @param type -
   *          the type of situation
   * @param currentPos -
   *          the current pos / ball pos
   * @param nextPos -
   *          the resulting pos /ball pos
   * @param security -
   *          the security in cycles in the resulting situation (time of
   *          opponent interception)
   * @param interceptionTime -
   *          the time of the situation - the current time (used only for ball
   *          interception)
   * @param p -
   *          the player who will have the ball in the resulting situation
   * @param wm -
   *          the world model (used only for debug output)
   * @return a value between 0..1 (1 best)
   */
  public final double getValue(final TYPE type,
      final Vektor currentPos,
      final Vektor nextPos,
      double security,
      final int interceptionTime,
      final Player p) {

    // always prefer getting near the goal
    final Vektor finalPosition = SConf.getInstance().GOAL_POS_OTHER;

    // the offenseFactBall is used for telling wether the ball moved
    // to a better position
    double offenseFactBall;

    // the offenseFactPlayer is used for telling wether the player moves
    // to a better position (i.e. passes which result in the teammate
    // getting nearer to the opponents goal are prefered to passes
    // in the back of an attacking teammate)
    double offenseFactPlayer;

    // the offense weight when in attack (get near opponents goal)
    if (currentPos.y > 30 || nextPos.y > 35) {
      offenseFactBall = currentPos.getDistance(finalPosition) - nextPos
          .getDistance(finalPosition);
      offenseFactPlayer = p.getPosition().getDistance(finalPosition) - nextPos
          .getDistance(finalPosition);
    }
    // the offense weight when in defense (get away from own goal)
    else if (currentPos.y < -20 || nextPos.y < -10) {
      offenseFactBall = nextPos.getDistance(SConf.getInstance().GOAL_POS_OWN) - currentPos
          .getDistance(SConf.getInstance().GOAL_POS_OWN);
      offenseFactPlayer = nextPos.getDistance(SConf.getInstance().GOAL_POS_OWN) - p
          .getDistance(SConf.getInstance().GOAL_POS_OWN);
    }
    // the offense weight when in midfield (go in Y-Dir)
    else {
      offenseFactBall = (nextPos.y - currentPos.y);
      offenseFactPlayer = (nextPos.y - p.getPosition().y);
    }

    // normalize weight
    offenseFactBall = Math.min(offenseFactBall, 30);
    offenseFactBall = Math.max(offenseFactBall, -30);
    offenseFactBall = offenseFactBall / 30.0 * 0.5 + 0.5;

    offenseFactPlayer = Math.min(offenseFactPlayer, 30);
    offenseFactPlayer = Math.max(offenseFactPlayer, -30);
    offenseFactPlayer = offenseFactPlayer / 30.0 * 0.5 + 0.5;

    offenseFactPlayer = Math.pow(offenseFactPlayer, 1.5);

    // if we are near the offside line the offense weight are pushed to
    // get in the back of the opponents defense
    if ((type == TYPE.PASSING || type == TYPE.BALL_INTERCEPTION || type == TYPE.KICKNRUN) 
    		&& (wm.getOffsideY(true) < nextPos.y + 3 || wm.inOpponentsPenalty(nextPos))) {
    	offenseFactBall = Math.pow(offenseFactBall, 0.7);
    	offenseFactPlayer = Math.pow(offenseFactPlayer, 0.7);
    }
    else if(type == TYPE.PASSING|| type == TYPE.BALL_INTERCEPTION || type == TYPE.KICKNRUN){
    	offenseFactBall = Math.pow(offenseFactBall, 0.85);
    	offenseFactPlayer = Math.pow(offenseFactPlayer, 0.85); 
        if(wm.getPlayMode() == PLAY_MODE.PLAY_ON
        	&&(wm.getBallRef().getPosition().y + 1 > wm.getOffsideY(true)
        		|| wm.getSelfRef().getPosition().y + 1 > wm.getOffsideY(true))
        		&& nextPos.y < currentPos.y){
        	offenseFactBall = Math.pow(offenseFactBall, 1.5);
        }
    }

    // how dangerous is the resulting situation
    // usd only for situations in the own field half
    double dangerFact = 1.0;
//    double dangerFact = nextPos.y < -25 ? 0.6 : nextPos.y < 0 ? 0.8 : 1.0;
    
    
    // danger depends on the action
    switch (type) {
      case DRIBBLING:
      case WAIT_WITH_BALL:
        // dangerFact = Math.pow(dangerFact, 0.5);
        break;

      // passing in front of the own goal without getting the ball away from the
      // goal is punished
      case PASSING:
        if (nextPos.y < -25 && Math.abs(nextPos.x) < 7) {
        	// System.out.println("HERE: REDUCED CASE" + this.wm.getCycleAndPlayerString());
          dangerFact /= nextPos.getDistance(SConf.getInstance().GOAL_POS_OWN) < 10 ? 10 : 1;
        }
        //don't pass back if you remain offside
        if(wm.getSelfRef().getPosition().y + 2 < wm.getOffsideY(true)){
        	if(Math.abs(currentPos.getAngleTo(nextPos))>120 && currentPos.y < 42 && currentPos.y > 0){
        		offenseFactBall /= 20;
        		// System.out.println("HERE: Reduced Result " + this.wm.getCycleAndPlayerString());
        	}
        }
    }

    dummyVektor.copy(nextPos);
    dummyVektor.subFromThis(p.getPosition());

    dummyVektor2.copy(finalPosition);
    dummyVektor2.subFromThis(p.getPosition());

    // try to avoid passes in the back of a teammate, this way we keep our team
    // moving towards the opponents goal
    
//    double passAngle= currentPos.getAngleTo(nextPos);
//    double interceptAngle= p.getPosition().getAngleTo(nextPos);
//    double angleDiff= Math.abs(Vektor.normalize(passAngle-interceptAngle));
//    if (angleDiff > 90) {
//    	angleDiff= 180-angleDiff;
//    }
//    double distFact= (currentPos.getDistance(nextPos))/(interceptionTime+1);

    
    
    double interceptAngleFact = dummyVektor
        .scalar(dummyVektor2) / (dummyVektor
        .getLength() * dummyVektor2.getLength());
    if (Double.isNaN(interceptAngleFact)) {
      interceptAngleFact = 1.0;
    }
    interceptAngleFact = interceptAngleFact * 0.2 + 0.8;

    // interceptAngleFact only needed for passing
    if (type == TYPE.DRIBBLING || type == TYPE.WAIT_WITH_BALL) {
      interceptAngleFact = 0.8;
    }

    // don't risk to loose the ball to the outside line
    double outFact = 0.5 + Math.min(SConf.getInstance().half_length - Math
        .abs(nextPos.y), 3) / 3 * Math.min(
        SConf.getInstance().half_width - Math.abs(nextPos.x), 3) / 3 * 0.5;

    if (outFact < 0) {
      outFact = 0;
    }

    outFact = Math.pow(outFact, (interceptionTime + 10) / 11);

    // security depends on the context, it is more important in the defense
    // and less important in front of the players goal as a too high security
    // level would prevent many successful scoring attempts.
    if (type == TYPE.PASSING) {

      // a pass from a defender to an attacker is prefered most
      // a pass rom an attacker to a defender should be avoided
      // therefore we manipulated the security based on the players' roles

      // passing player
      // attacking -> more security
      if (p.getRoleNumber() > 5) {
        security += 1.0;
      }
      // defending -> less security
      else if (p.getRoleNumber() < 6) {
        security -= 1.5;
      }

      // receiver
      // attacking -> more security
      if (wm.getSelfRef().getRoleNumber() > 5) {
        security -= 1.0;
      }
      // defending -> less security
      else if (wm.getSelfRef().getRoleNumber() < 6) {
        security += 0.5;
      }

      final double goalDistOther = nextPos
          .getDistance(SConf.getInstance().GOAL_POS_OTHER);

      // if near the opponents goal risk something ...
      if (goalDistOther < 8) {
        security = 3;
      }
      else if (goalDistOther < 12) {
        security += 2;
      }
      // if near the goal or near the offside line
      else if (goalDistOther < 16 || wm.getOffsideY(true) < nextPos.y) {
        security += 1;
      }
    }

    // we wait with the ball in standards till secure passes are possible
    // as the opponent can't get near the ball
    if (type == TYPE.WAIT_WITH_BALL && (wm.getPlayMode() == PLAY_MODE.KICK_IN_OWN || wm
        .getPlayMode() == PLAY_MODE.CORNER_KICK_OWN)) {
      if (currentPos.y > 0) {
        security -= 2;
      }
    }

    // normalize
    security = Math.max(security, 0);
    security = Math.min(security, 3);

    double secureFact = (1.0 + security) / 4.0;
    if(type == TYPE.PASSING){
    	secureFact = Math.pow(secureFact, 1.2);
    }

    // without stamina attacking makes less sense -> waiting or passing should
    // be prefered
    double staminaFact = 1;

    switch (type) {
      case KICKNRUN:
        double distGoal = p.getDistance(SConf.getInstance().GOAL_POS_OTHER);

        // getting in the back o the defense is very interesting and we care
        // less about stamina
        if (p.getPosition().y > wm.getOffsideY(true) - 10) {
          distGoal -= Math.min(distGoal, 10);

          final double staminaLeft = p.getStamina() - distGoal * 25;

          staminaFact *= Math.min(1, staminaLeft / 1400.0);
        }
        // normal kickNrun is of less interest and stamina more important
        else {
          double staminaLevel = 2500;
          if (distGoal < 15) {
            staminaLevel = 1300;
          }
          else if (distGoal < 25) {
            staminaLevel = 1800;
          }

          if (p.getStamina() < staminaLevel) {
            staminaFact = 0.15;
          }
        }
        break;

      // avoid passing to teammates with low stamina
      case PASSING:
        distGoal = p.getDistance(SConf.getInstance().GOAL_POS_OTHER);

        double staminaLevel = 2400;
        if (distGoal < 10) {
          staminaLevel = 0;
        }
        else if (distGoal < 15) {
          staminaLevel = 800;
        }
        else if (distGoal < 25) {
          staminaLevel = 1300;
        }

        if (p.getStamina() < staminaLevel) {
          staminaFact = 0.5;
        }

        break;
    }

    // avoid passing to teammates whith only old knowledge about their position
    double playerAgeFact = 1.0;

    if (type == TYPE.PASSING) {
      playerAgeFact = Math.pow(0.85, p.getAge(wm.getBodyCycle(),
          UPDATE_MODE.META_BASED_ON_INFO));
    }
    // if kickNrun avoid to choose directions not seen since a long time
    else if (type == TYPE.KICKNRUN) {
      playerAgeFact = Math.pow(0.9, wm.getAgeOfPoint(nextPos));
    }

    // adapt to game score, be more aggressive when we are loosing and more
    // save when we are winning
    if (wm.getGoalDiff() > 0) {
      offenseFactPlayer = Math.pow(offenseFactPlayer, 1.2);
      offenseFactBall = Math.pow(offenseFactBall, 1.1);
    }
    else if (wm.getGoalDiff() == 0) {
      offenseFactPlayer = Math.pow(offenseFactPlayer, 1.3);
      offenseFactBall = Math.pow(offenseFactBall, 1.2);
    }
    else {
      offenseFactPlayer = Math.pow(offenseFactPlayer, 1.4);
      offenseFactBall = Math.pow(offenseFactBall, 1.3);
    }

    // if one side of the field is full of opponents we try to get the ball
    // to the other side (passing)
    double wingFact = 1.0;

    if (type == TYPE.PASSING && currentPos.getDistance(nextPos) > 0.5) {
      final double angleToGo = currentPos.getAngleTo(nextPos);

      switch (wm.getDominatedSide()) {
        case RIGHT:
          if (RCMath.isBetween(angleToGo, -135, -30)) {
            wingFact = 1.2;
          }
          break;

        case LEFT:
          if (RCMath.isBetween(angleToGo, 30.0, -135.0)) {
            wingFact = 1.2;
          }
          break;

        case NONE:
          if (RCMath.isBetween(angleToGo, 30.0, -135.0) || RCMath.isBetween(
              angleToGo, -135, -30)) {
            wingFact = 1.2;
          }
          break;
      }
    }

    // goalie has some logic of its own
    if (wm.getSelfRef().isGoalie()) {

      // don't be offensive ;-)
      offenseFactPlayer = 1.0;

      // try to avoid passing to the center instead prefer wings
      if (type == TYPE.PASSING) {
        final double dirAngle = currentPos.getAngleTo(nextPos);
        if (Math.abs(dirAngle) > 100) {
          return -0.12;
        }
        wingFact = 1.0;

      }

      // security is more important
      // secureFact *= secureFact;

      // interceptAngleFact is of no importance
      interceptAngleFact = 1.0;

      // the passing to lately seen players is prefered
      playerAgeFact *= playerAgeFact;
    }
    
    double scoreAngleFact= 1;
    // when near goal, a good shot angle is important
    if (currentPos.y>35 || nextPos.y > 40) {
      final double angleLeft= nextPos.getAngleTo(SConf.getInstance().GOAL_POST_LEFT_OTHER);
      final double angleRight= nextPos.getAngleTo(SConf.getInstance().GOAL_POST_RIGHT_OTHER);
      if (wm.getOpponentsGoalie() == null) {
        scoreAngleFact= (angleRight-angleLeft)/60;
      }
      else {
        final double goalieAngle= nextPos.getAngleTo(wm.getOpponentsGoalie().getPosition());
        if (goalieAngle < angleLeft || goalieAngle > angleRight) {
          scoreAngleFact= (angleRight-angleLeft)/60;
        }
        else if (angleRight-goalieAngle > goalieAngle-angleLeft) {
          scoreAngleFact= (angleRight-goalieAngle)/60;
        }
        else {
          scoreAngleFact= (goalieAngle-angleLeft)/60;
        }
      }
    }
    scoreAngleFact= Math.max(0,Math.min(1, scoreAngleFact));

    // get the final benefit value
    double value = offenseFactBall * offenseFactPlayer * outFact * secureFact * interceptAngleFact * staminaFact * dangerFact * playerAgeFact * wingFact * scoreAngleFact;

    value = Math.pow(value, weights.get(type));
    
    return value;
  }
}
